/*
Copyright 2008-2010 Gephi
Authors : Mathieu Bastian <mathieu.bastian@gephi.org>
Website : http://www.gephi.org

This file is part of Gephi.

Gephi is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

Gephi is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Gephi.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.gephi.partition.impl;

import com.google.common.collect.ArrayListMultimap;
import java.awt.Color;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import org.gephi.data.attributes.api.AttributeColumn;
import org.gephi.data.attributes.api.AttributeType;
import org.gephi.data.attributes.api.Estimator;
import org.gephi.data.attributes.type.DynamicType;
import org.gephi.data.attributes.type.TimeInterval;
import org.gephi.graph.api.Edge;
import org.gephi.graph.api.EdgeData;
import org.gephi.graph.api.Graph;
import org.gephi.graph.api.Node;
import org.gephi.graph.api.NodeData;
import org.gephi.partition.api.EdgePartition;
import org.gephi.partition.api.NodePartition;
import org.gephi.partition.api.Part;
import org.gephi.partition.api.Partition;

/**
 *
 * @author Mathieu Bastian
 */
public class PartitionFactory {

    public static boolean isPartitionColumn(AttributeColumn column) {
        return column.getType().equals(AttributeType.STRING)
                || column.getType().equals(AttributeType.BOOLEAN)
                || column.getType().equals(AttributeType.INT)
                || column.getType().equals(AttributeType.SHORT);
    }

    public static boolean isDynamicPartitionColumn(AttributeColumn column) {
        return column.getType().equals(AttributeType.DYNAMIC_STRING)
                || column.getType().equals(AttributeType.DYNAMIC_BOOLEAN)
                || column.getType().equals(AttributeType.DYNAMIC_INT)
                || column.getType().equals(AttributeType.DYNAMIC_SHORT);
    }

    public static boolean isNodePartitionColumn(AttributeColumn column, Graph graph) {
        Set values = new HashSet();
        int nonNullvalues = 0;
        for (Node n : graph.getNodes()) {
            Object value = n.getNodeData().getAttributes().getValue(column.getIndex());
            if (value != null) {
                nonNullvalues++;
            }
            values.add(value);
        }
        if (values.size() < 9f / 10f * nonNullvalues) {      //If #different values is < 9:10 of total non-null values
            return true;
        }
        return false;
    }

    public static boolean isEdgePartitionColumn(AttributeColumn column, Graph graph) {
        Set values = new HashSet();
        int nonNullvalues = 0;
        for (Edge n : graph.getEdges()) {
            Object value = n.getEdgeData().getAttributes().getValue(column.getIndex());
            if (value != null) {
                nonNullvalues++;
            }
            values.add(value);
        }
        if (values.size() < 9f / 10f * nonNullvalues) {      //If #different values is < 9:10 of total non-null values
            return true;
        }
        return false;
    }

    public static boolean isDynamicNodePartitionColumn(AttributeColumn column, Graph graph, TimeInterval timeInterval, Estimator estimator) {
        Set values = new HashSet();
        int nonNullvalues = 0;
        for (Node n : graph.getNodes()) {
            Object value = n.getNodeData().getAttributes().getValue(column.getIndex());
            value = getDynamicValue(value, timeInterval, estimator);
            if (value != null) {
                nonNullvalues++;
            }
            values.add(value);
        }
        if (values.size() < 9f / 10f * nonNullvalues) {      //If #different values is < 9:10 of total non-null values
            return true;
        }
        return false;
    }

    public static boolean isDynamicEdgePartitionColumn(AttributeColumn column, Graph graph, TimeInterval timeInterval, Estimator estimator) {
        Set values = new HashSet();
        int nonNullvalues = 0;
        for (Edge n : graph.getEdges()) {
            Object value = n.getEdgeData().getAttributes().getValue(column.getIndex());
            value = getDynamicValue(value, timeInterval, estimator);
            if (value != null) {
                nonNullvalues++;
            }
            values.add(value);
        }
        if (values.size() < 9f / 10f * nonNullvalues) {      //If #different values is < 9:10 of total non-null values
            return true;
        }
        return false;
    }

    private static Object getDynamicValue(Object object, TimeInterval timeInterval, Estimator estimator) {
        if (object != null && object instanceof DynamicType) {
            DynamicType dynamicType = (DynamicType) object;
            return dynamicType.getValue(timeInterval == null ? Double.NEGATIVE_INFINITY : timeInterval.getLow(),
                    timeInterval == null ? Double.POSITIVE_INFINITY : timeInterval.getHigh(), estimator);
        }
        return object;
    }

    public static NodePartition createNodePartition(AttributeColumn column) {
        return new NodePartitionImpl(column);
    }

    public static EdgePartition createEdgePartition(AttributeColumn column) {
        return new EdgePartitionImpl(column);
    }

    public static boolean isPartitionBuilt(Partition partition) {
        return partition.getParts().length > 0;
    }

    public static void buildNodePartition(NodePartition partition, Graph graph) {
        buildNodePartition(partition, graph, null, null);
    }

    public static void buildNodePartition(NodePartition partition, Graph graph, TimeInterval timeInterval, Estimator estimator) {

        NodePartitionImpl partitionImpl = (NodePartitionImpl) partition;
        ArrayListMultimap<Object, Node> multimap = ArrayListMultimap.create();
        for (Node n : graph.getNodes()) {
            Object value = n.getNodeData().getAttributes().getValue(partitionImpl.column.getIndex());
            value = getDynamicValue(value, timeInterval, estimator);
            multimap.put(value, n);
        }

        PartImpl<Node>[] parts = new PartImpl[multimap.keySet().size()];
        Map<Object, Collection<Node>> map = multimap.asMap();
        int i = 0;
        for (Entry<Object, Collection<Node>> entry : map.entrySet()) {
            PartImpl<Node> part = new PartImpl<Node>(partition, entry.getKey(), entry.getValue().toArray(new Node[0]));
            parts[i] = part;
            i++;
        }
        partitionImpl.setParts(parts);
    }

    public static void buildEdgePartition(EdgePartition partition, Graph graph) {
        buildEdgePartition(partition, graph, null, null);
    }

    public static void buildEdgePartition(EdgePartition partition, Graph graph, TimeInterval timeInterval, Estimator estimator) {
        EdgePartitionImpl partitionImpl = (EdgePartitionImpl) partition;

        ArrayListMultimap<Object, Edge> multimap = ArrayListMultimap.create();
        for (Edge n : graph.getEdges()) {
            Object value = n.getEdgeData().getAttributes().getValue(partitionImpl.column.getIndex());
            value = getDynamicValue(value, timeInterval, estimator);
            multimap.put(value, n);
        }

        PartImpl<Edge>[] parts = new PartImpl[multimap.keySet().size()];
        Map<Object, Collection<Edge>> map = multimap.asMap();
        int i = 0;
        for (Entry<Object, Collection<Edge>> entry : map.entrySet()) {
            PartImpl<Edge> part = new PartImpl<Edge>(partition, entry.getKey(), entry.getValue().toArray(new Edge[0]));
            parts[i] = part;
            i++;
        }
        partitionImpl.setParts(parts);
    }

    private static class NodePartitionImpl implements NodePartition {

        private HashMap<NodeData, Part<Node>> nodeMap;
        private HashMap<Object, Part<Node>> valueMap;
        private PartImpl<Node>[] parts;
        private AttributeColumn column;

        public NodePartitionImpl(AttributeColumn column) {
            this.column = column;
            nodeMap = new HashMap<NodeData, Part<Node>>();
            valueMap = new HashMap<Object, Part<Node>>();
            parts = new PartImpl[0];
        }

        public int getPartsCount() {
            return parts.length;
        }

        public Part<Node> getPartFromValue(Object value) {
            return valueMap.get(value);
        }

        public Part<Node>[] getParts() {
            return parts;
        }

        public Map<NodeData, Part<Node>> getMap() {
            return nodeMap;
        }

        public Part<Node> getPart(Node element) {
            return nodeMap.get(element.getNodeData());
        }

        public void setParts(PartImpl<Node>[] parts) {
            this.parts = parts;
            List<Color> colors = getSequenceColors(parts.length);
            int i = 0;
            for (PartImpl<Node> p : parts) {
                for (Node n : p.objects) {
                    nodeMap.put(n.getNodeData(), p);
                }
                p.setColor(colors.get(i));
                valueMap.put(p.getValue(), p);
                i++;
            }
        }

        public AttributeColumn getColumn() {
            return column;
        }

        @Override
        public String toString() {
            return column.getTitle();
        }

        public int getElementsCount() {
            return nodeMap.size();
        }
    }

    private static class EdgePartitionImpl implements EdgePartition {

        private HashMap<EdgeData, Part<Edge>> edgeMap;
        private PartImpl<Edge>[] parts;
        private HashMap<Object, Part<Edge>> valueMap;
        private AttributeColumn column;

        public EdgePartitionImpl(AttributeColumn column) {
            this.column = column;
            edgeMap = new HashMap<EdgeData, Part<Edge>>();
            valueMap = new HashMap<Object, Part<Edge>>();
            parts = new PartImpl[0];
        }

        public int getPartsCount() {
            return parts.length;
        }

        public Part<Edge>[] getParts() {
            return parts;
        }

        public Part<Edge> getPartFromValue(Object value) {
            return valueMap.get(value);
        }

        public Map<EdgeData, Part<Edge>> getMap() {
            return edgeMap;
        }

        public Part<Edge> getPart(Edge element) {
            return edgeMap.get(element.getEdgeData());
        }

        public void setParts(PartImpl<Edge>[] parts) {
            this.parts = parts;
            List<Color> colors = getSequenceColors(parts.length);
            int i = 0;
            for (PartImpl<Edge> p : parts) {
                for (Edge e : p.objects) {
                    edgeMap.put(e.getEdgeData(), p);
                }
                p.setColor(colors.get(i));
                valueMap.put(p.getValue(), p);
                i++;
            }
        }

        public AttributeColumn getColumn() {
            return column;
        }

        @Override
        public String toString() {
            return column.getTitle();
        }

        public int getElementsCount() {
            return edgeMap.size();
        }
    }

    private static class PartImpl<Element> implements Part<Element> {

        private static final String NULL = "null";
        private Partition<Element> partition;
        private Element[] objects;
        private Object value;
        private Color color;

        public PartImpl(Partition<Element> partition, Object value, Element[] objects) {
            this.partition = partition;
            this.value = value;
            this.objects = objects;
        }

        public Element[] getObjects() {
            return objects;
        }

        public Object getValue() {
            return value;
        }

        public String getDisplayName() {
            return value != null ? value.toString() : NULL;
        }

        public boolean isInPart(Element element) {
            return partition.getPart(element) == this;
        }

        public void setColor(Color color) {
            this.color = color;
        }

        public Color getColor() {
            return color;
        }

        public float getPercentage() {
            return objects.length / (float) partition.getElementsCount();
        }

        public Partition getPartition() {
            return partition;
        }

        @Override
        public String toString() {
            return getDisplayName();
        }

        public int compareTo(Object o) {
            int thisCount = objects.length;
            int theirCount = ((PartImpl) o).objects.length;
            return thisCount == theirCount ? 0 : thisCount > theirCount ? 1 : -1;
        }
    }

    public static List<Color> getSequenceColors(int num) {
        List<Color> colors = new LinkedList<Color>();

        //On choisit H et S au random
        Random random = new Random();
        float B = random.nextFloat() * 2 / 5f + 0.6f;		//		0.6 <=   B   < 1
        float S = random.nextFloat() * 2 / 5f + 0.6f;		//		0.6 <=   S   < 1
        //System.out.println("B : "+B+"  S : "+S);

        for (int i = 1; i <= num; i++) {
            float H = i / (float) num;
            //System.out.println(H);
            Color c = Color.getHSBColor(H, S, B);
            colors.add(c);
        }

        Collections.shuffle(colors);

        return colors;
    }
}
